<?php
/**
 * CActiveFinder class file.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @link http://www.yiiframework.com/
 * @copyright 2008-2013 Yii Software LLC
 * @license http://www.yiiframework.com/license/
 */

/**
 * CActiveFinder implements eager loading and lazy loading of related active records.
 *
 * When used in eager loading, this class provides the same set of find methods as
 * {@link CActiveRecord}.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.db.ar
 * @since 1.0
 */
class CActiveFinder extends CComponent
{
    /**
     * @var boolean join all tables all at once. Defaults to false.
     * This property is internally used.
     */
    public $joinAll = false;
    /**
     * @var boolean whether the base model has limit or offset.
     * This property is internally used.
     */
    public $baseLimited = false;

    private $_joinCount = 0;
    private $_joinTree;
    private $_builder;

    /**
     * Constructor.
     * A join tree is built up based on the declared relationships between active record classes.
     * @param CActiveRecord $model the model that initiates the active finding process
     * @param mixed $with the relation names to be actively looked for
     */
    public function __construct($model, $with)
    {
        $this->_builder = $model->getCommandBuilder();
        $this->_joinTree = new CJoinElement($this, $model);
        $this->buildJoinTree($this->_joinTree, $with);
    }

    /**
     * Do not call this method. This method is used internally to perform the relational query
     * based on the given DB criteria.
     * @param CDbCriteria $criteria the DB criteria
     * @param boolean $all whether to bring back all records
     * @return mixed the query result
     */
    public function query($criteria, $all = false)
    {
        $this->joinAll = $criteria->together === true;

        if ($criteria->alias != '') {
            $this->_joinTree->tableAlias = $criteria->alias;
            $this->_joinTree->rawTableAlias = $this->_builder->getSchema()->quoteTableName($criteria->alias);
        }

        $this->_joinTree->find($criteria);
        $this->_joinTree->afterFind();

        if ($all) {
            $result = array_values($this->_joinTree->records);
            if ($criteria->index !== null) {
                $index = $criteria->index;
                $array = array();
                foreach ($result as $object)
                    $array[$object->$index] = $object;
                $result = $array;
            }
        } elseif (count($this->_joinTree->records))
            $result = reset($this->_joinTree->records);
        else
            $result = null;

        $this->destroyJoinTree();
        return $result;
    }

    /**
     * This method is internally called.
     * @param string $sql the SQL statement
     * @param array $params parameters to be bound to the SQL statement
     * @return CActiveRecord
     */
    public function findBySql($sql, $params = array())
    {
        Yii::trace(get_class($this->_joinTree->model) . '.findBySql() eagerly', 'system.db.ar.CActiveRecord');
        if (($row = $this->_builder->createSqlCommand($sql, $params)->queryRow()) !== false) {
            $baseRecord = $this->_joinTree->model->populateRecord($row, false);
            $this->_joinTree->findWithBase($baseRecord);
            $this->_joinTree->afterFind();
            $this->destroyJoinTree();
            return $baseRecord;
        } else
            $this->destroyJoinTree();
    }

    /**
     * This method is internally called.
     * @param string $sql the SQL statement
     * @param array $params parameters to be bound to the SQL statement
     * @return CActiveRecord[]
     */
    public function findAllBySql($sql, $params = array())
    {
        Yii::trace(get_class($this->_joinTree->model) . '.findAllBySql() eagerly', 'system.db.ar.CActiveRecord');
        if (($rows = $this->_builder->createSqlCommand($sql, $params)->queryAll()) !== array()) {
            $baseRecords = $this->_joinTree->model->populateRecords($rows, false);
            $this->_joinTree->findWithBase($baseRecords);
            $this->_joinTree->afterFind();
            $this->destroyJoinTree();
            return $baseRecords;
        } else {
            $this->destroyJoinTree();
            return array();
        }
    }

    /**
     * This method is internally called.
     * @param CDbCriteria $criteria the query criteria
     * @return string
     */
    public function count($criteria)
    {
        Yii::trace(get_class($this->_joinTree->model) . '.count() eagerly', 'system.db.ar.CActiveRecord');
        $this->joinAll = $criteria->together !== true;

        $alias = $criteria->alias === null ? 't' : $criteria->alias;
        $this->_joinTree->tableAlias = $alias;
        $this->_joinTree->rawTableAlias = $this->_builder->getSchema()->quoteTableName($alias);

        $n = $this->_joinTree->count($criteria);
        $this->destroyJoinTree();
        return $n;
    }

    /**
     * Finds the related objects for the specified active record.
     * This method is internally invoked by {@link CActiveRecord} to support lazy loading.
     * @param CActiveRecord $baseRecord the base record whose related objects are to be loaded
     */
    public function lazyFind($baseRecord)
    {
        $this->_joinTree->lazyFind($baseRecord);
        if (!empty($this->_joinTree->children)) {
            foreach ($this->_joinTree->children as $child)
                $child->afterFind();
        }
        $this->destroyJoinTree();
    }

    /**
     * Given active record class name returns new model instance.
     *
     * @param string $className active record class name
     * @return CActiveRecord active record model instance
     *
     * @since 1.1.14
     */
    public function getModel($className)
    {
        return CActiveRecord::model($className);
    }

    private function destroyJoinTree()
    {
        if ($this->_joinTree !== null)
            $this->_joinTree->destroy();
        $this->_joinTree = null;
    }

    /**
     * Builds up the join tree representing the relationships involved in this query.
     * @param CJoinElement $parent the parent tree node
     * @param mixed $with the names of the related objects relative to the parent tree node
     * @param array $options additional query options to be merged with the relation
     * @throws CDbException if given parent tree node is an instance of {@link CStatElement}
     * or relation is not defined in the given parent's tree node model class
     */
    private function buildJoinTree($parent, $with, $options = null)
    {
        if ($parent instanceof CStatElement)
            throw new CDbException(Yii::t('yii', 'The STAT relation "{name}" cannot have child relations.',
                array('{name}' => $parent->relation->name)));

        if (is_string($with)) {
            if (($pos = strrpos($with, '.')) !== false) {
                $parent = $this->buildJoinTree($parent, substr($with, 0, $pos));
                $with = substr($with, $pos + 1);
            }

            // named scope
            $scopes = array();
            if (($pos = strpos($with, ':')) !== false) {
                $scopes = explode(':', substr($with, $pos + 1));
                $with = substr($with, 0, $pos);
            }

            if (isset($parent->children[$with]) && $parent->children[$with]->master === null)
                return $parent->children[$with];

            if (($relation = $parent->model->getActiveRelation($with)) === null)
                throw new CDbException(Yii::t('yii', 'Relation "{name}" is not defined in active record class "{class}".',
                    array('{class}' => get_class($parent->model), '{name}' => $with)));

            $relation = clone $relation;
            $model = $this->getModel($relation->className);

            if ($relation instanceof CActiveRelation) {
                $oldAlias = $model->getTableAlias(false, false);
                if (isset($options['alias']))
                    $model->setTableAlias($options['alias']);
                elseif ($relation->alias === null)
                    $model->setTableAlias($relation->name);
                else
                    $model->setTableAlias($relation->alias);
            }

            if (!empty($relation->scopes))
                $scopes = array_merge($scopes, (array)$relation->scopes); // no need for complex merging

            if (!empty($options['scopes']))
                $scopes = array_merge($scopes, (array)$options['scopes']); // no need for complex merging

            $model->resetScope(false);
            $criteria = $model->getDbCriteria();
            $criteria->scopes = $scopes;
            $model->beforeFindInternal();
            $model->applyScopes($criteria);

            // select has a special meaning in stat relation, so we need to ignore select from scope or model criteria
            if ($relation instanceof CStatRelation)
                $criteria->select = '*';

            $relation->mergeWith($criteria, true);

            // dynamic options
            if ($options !== null)
                $relation->mergeWith($options);

            if ($relation instanceof CActiveRelation)
                $model->setTableAlias($oldAlias);

            if ($relation instanceof CStatRelation)
                return new CStatElement($this, $relation, $parent);
            else {
                if (isset($parent->children[$with])) {
                    $element = $parent->children[$with];
                    $element->relation = $relation;
                } else
                    $element = new CJoinElement($this, $relation, $parent, ++$this->_joinCount);
                if (!empty($relation->through)) {
                    $slave = $this->buildJoinTree($parent, $relation->through, array('select' => ''));
                    $slave->master = $element;
                    $element->slave = $slave;
                }
                $parent->children[$with] = $element;
                if (!empty($relation->with))
                    $this->buildJoinTree($element, $relation->with);
                return $element;
            }
        }

        // $with is an array, keys are relation name, values are relation spec
        foreach ($with as $key => $value) {
            if (is_string($value)) // the value is a relation name
                $this->buildJoinTree($parent, $value);
            elseif (is_string($key) && is_array($value))
                $this->buildJoinTree($parent, $key, $value);
        }
    }
}


/**
 * CJoinElement represents a tree node in the join tree created by {@link CActiveFinder}.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.db.ar
 * @since 1.0
 */
class CJoinElement
{
    /**
     * @var integer the unique ID of this tree node
     */
    public $id;
    /**
     * @var CActiveRelation the relation represented by this tree node
     */
    public $relation;
    /**
     * @var CActiveRelation the master relation
     */
    public $master;
    /**
     * @var CActiveRelation the slave relation
     */
    public $slave;
    /**
     * @var CActiveRecord the model associated with this tree node
     */
    public $model;
    /**
     * @var array list of active records found by the queries. They are indexed by primary key values.
     */
    public $records = array();
    /**
     * @var array list of child join elements
     */
    public $children = array();
    /**
     * @var array list of stat elements
     */
    public $stats = array();
    /**
     * @var string table alias for this join element
     */
    public $tableAlias;
    /**
     * @var string the quoted table alias for this element
     */
    public $rawTableAlias;

    private $_finder;
    private $_builder;
    private $_parent;
    private $_pkAlias; // string or name=>alias
    private $_columnAliases = array(); // name=>alias
    private $_joined = false;
    private $_table;
    private $_related = array(); // PK, relation name, related PK => true

    /**
     * Constructor.
     * @param CActiveFinder $finder the finder
     * @param mixed $relation the relation (if the third parameter is not null)
     * or the model (if the third parameter is null) associated with this tree node.
     * @param CJoinElement $parent the parent tree node
     * @param integer $id the ID of this tree node that is unique among all the tree nodes
     */
    public function __construct($finder, $relation, $parent = null, $id = 0)
    {
        $this->_finder = $finder;
        $this->id = $id;
        if ($parent !== null) {
            $this->relation = $relation;
            $this->_parent = $parent;
            $this->model = $this->_finder->getModel($relation->className);
            $this->_builder = $this->model->getCommandBuilder();
            $this->tableAlias = $relation->alias === null ? $relation->name : $relation->alias;
            $this->rawTableAlias = $this->_builder->getSchema()->quoteTableName($this->tableAlias);
            $this->_table = $this->model->getTableSchema();
        } else // root element, the first parameter is the model.
        {
            $this->model = $relation;
            $this->_builder = $relation->getCommandBuilder();
            $this->_table = $relation->getTableSchema();
            $this->tableAlias = $this->model->getTableAlias();
            $this->rawTableAlias = $this->_builder->getSchema()->quoteTableName($this->tableAlias);
        }

        // set up column aliases, such as t1_c2
        $table = $this->_table;
        if ($this->model->getDbConnection()->getDriverName() === 'oci') // Issue 482
            $prefix = 'T' . $id . '_C';
        else
            $prefix = 't' . $id . '_c';
        foreach ($table->getColumnNames() as $key => $name) {
            $alias = $prefix . $key;
            $this->_columnAliases[$name] = $alias;
            if ($table->primaryKey === $name)
                $this->_pkAlias = $alias;
            elseif (is_array($table->primaryKey) && in_array($name, $table->primaryKey))
                $this->_pkAlias[$name] = $alias;
        }
    }

    /**
     * Removes references to child elements and finder to avoid circular references.
     * This is internally used.
     */
    public function destroy()
    {
        if (!empty($this->children)) {
            foreach ($this->children as $child)
                $child->destroy();
        }
        unset($this->_finder, $this->_parent, $this->model, $this->relation, $this->master, $this->slave, $this->records, $this->children, $this->stats);
    }

    /**
     * Performs the recursive finding with the criteria.
     * @param CDbCriteria $criteria the query criteria
     */
    public function find($criteria = null)
    {
        if ($this->_parent === null) // root element
        {
            $query = new CJoinQuery($this, $criteria);
            $this->_finder->baseLimited = ($criteria->offset >= 0 || $criteria->limit >= 0);
            $this->buildQuery($query);
            $this->_finder->baseLimited = false;
            $this->runQuery($query);
        } elseif (!$this->_joined && !empty($this->_parent->records)) // not joined before
        {
            $query = new CJoinQuery($this->_parent);
            $this->_joined = true;
            $query->join($this);
            $this->buildQuery($query);
            $this->_parent->runQuery($query);
        }

        foreach ($this->children as $child) // find recursively
            $child->find();

        foreach ($this->stats as $stat)
            $stat->query();
    }

    /**
     * Performs lazy find with the specified base record.
     * @param CActiveRecord $baseRecord the active record whose related object is to be fetched.
     */
    public function lazyFind($baseRecord)
    {
        if (is_string($this->_table->primaryKey))
            $this->records[$baseRecord->{$this->_table->primaryKey}] = $baseRecord;
        else {
            $pk = array();
            foreach ($this->_table->primaryKey as $name)
                $pk[$name] = $baseRecord->$name;
            $this->records[serialize($pk)] = $baseRecord;
        }

        foreach ($this->stats as $stat)
            $stat->query();

        if (!$this->children)
            return;

        $params = array();
        foreach ($this->children as $child)
            if (is_array($child->relation->params))
                $params = array_merge($params, $child->relation->params);

        $query = new CJoinQuery($child);
        $query->selects = array($child->getColumnSelect($child->relation->select));
        $query->conditions = array(
            $child->relation->condition,
            $child->relation->on,
        );
        $query->groups[] = $child->relation->group;
        $query->joins[] = $child->relation->join;
        $query->havings[] = $child->relation->having;
        $query->orders[] = $child->relation->order;
        $query->params = $params;
        $query->elements[$child->id] = true;
        if ($child->relation instanceof CHasManyRelation) {
            $query->limit = $child->relation->limit;
            $query->offset = $child->relation->offset;
        }

        $child->applyLazyCondition($query, $baseRecord);

        $this->_joined = true;
        $child->_joined = true;

        $this->_finder->baseLimited = false;
        $child->buildQuery($query);
        $child->runQuery($query);
        foreach ($child->children as $c)
            $c->find();

        if (empty($child->records))
            return;
        if ($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation)
            $baseRecord->addRelatedRecord($child->relation->name, reset($child->records), false);
        else // has_many and many_many
        {
            foreach ($child->records as $record) {
                if ($child->relation->index !== null)
                    $index = $record->{$child->relation->index};
                else
                    $index = true;
                $baseRecord->addRelatedRecord($child->relation->name, $record, $index);
            }
        }
    }

    /**
     * Apply Lazy Condition
     * @param CJoinQuery $query represents a JOIN SQL statements
     * @param CActiveRecord $record the active record whose related object is to be fetched.
     * @throws CDbException if relation in active record class is not specified correctly
     */
    private function applyLazyCondition($query, $record)
    {
        $schema = $this->_builder->getSchema();
        $parent = $this->_parent;
        if ($this->relation instanceof CManyManyRelation) {
            $joinTableName = $this->relation->getJunctionTableName();
            if (($joinTable = $schema->getTable($joinTableName)) === null)
                throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.',
                    array('{class}' => get_class($parent->model), '{relation}' => $this->relation->name, '{joinTable}' => $joinTableName)));
            $fks = $this->relation->getJunctionForeignKeys();

            $joinAlias = $schema->quoteTableName($this->relation->name . '_' . $this->tableAlias);
            $parentCondition = array();
            $childCondition = array();
            $count = 0;
            $params = array();

            $fkDefined = true;
            foreach ($fks as $i => $fk) {
                if (isset($joinTable->foreignKeys[$fk])) // FK defined
                {
                    list($tableName, $pk) = $joinTable->foreignKeys[$fk];
                    if (!isset($parentCondition[$pk]) && $schema->compareTableNames($parent->_table->rawName, $tableName)) {
                        $parentCondition[$pk] = $joinAlias . '.' . $schema->quoteColumnName($fk) . '=:ypl' . $count;
                        $params[':ypl' . $count] = $record->$pk;
                        $count++;
                    } elseif (!isset($childCondition[$pk]) && $schema->compareTableNames($this->_table->rawName, $tableName))
                        $childCondition[$pk] = $this->getColumnPrefix() . $schema->quoteColumnName($pk) . '=' . $joinAlias . '.' . $schema->quoteColumnName($fk);
                    else {
                        $fkDefined = false;
                        break;
                    }
                } else {
                    $fkDefined = false;
                    break;
                }
            }

            if (!$fkDefined) {
                $parentCondition = array();
                $childCondition = array();
                $count = 0;
                $params = array();
                foreach ($fks as $i => $fk) {
                    if ($i < count($parent->_table->primaryKey)) {
                        $pk = is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey;
                        $parentCondition[$pk] = $joinAlias . '.' . $schema->quoteColumnName($fk) . '=:ypl' . $count;
                        $params[':ypl' . $count] = $record->$pk;
                        $count++;
                    } else {
                        $j = $i - count($parent->_table->primaryKey);
                        $pk = is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey;
                        $childCondition[$pk] = $this->getColumnPrefix() . $schema->quoteColumnName($pk) . '=' . $joinAlias . '.' . $schema->quoteColumnName($fk);
                    }
                }
            }

            if ($parentCondition !== array() && $childCondition !== array()) {
                $join = 'INNER JOIN ' . $joinTable->rawName . ' ' . $joinAlias . ' ON ';
                $join .= '(' . implode(') AND (', $parentCondition) . ') AND (' . implode(') AND (', $childCondition) . ')';
                if (!empty($this->relation->on))
                    $join .= ' AND (' . $this->relation->on . ')';
                $query->joins[] = $join;
                foreach ($params as $name => $value)
                    $query->params[$name] = $value;
            } else
                throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
                    array('{class}' => get_class($parent->model), '{relation}' => $this->relation->name)));
        } else {
            $element = $this;
            while (true) {
                $condition = $element->relation->condition;
                if (!empty($condition))
                    $query->conditions[] = $condition;
                $query->params = array_merge($query->params, $element->relation->params);
                if ($element->slave !== null) {
                    $query->joins[] = $element->slave->joinOneMany($element->slave, $element->relation->foreignKey, $element, $parent);
                    $element = $element->slave;
                } else
                    break;
            }
            $fks = is_array($element->relation->foreignKey) ? $element->relation->foreignKey : preg_split('/\s*,\s*/', $element->relation->foreignKey, -1, PREG_SPLIT_NO_EMPTY);
            $prefix = $element->getColumnPrefix();
            $params = array();
            foreach ($fks as $i => $fk) {
                if (!is_int($i)) {
                    $pk = $fk;
                    $fk = $i;
                }

                if ($element->relation instanceof CBelongsToRelation) {
                    if (is_int($i)) {
                        if (isset($parent->_table->foreignKeys[$fk])) // FK defined
                            $pk = $parent->_table->foreignKeys[$fk][1];
                        elseif (is_array($element->_table->primaryKey)) // composite PK
                            $pk = $element->_table->primaryKey[$i];
                        else
                            $pk = $element->_table->primaryKey;
                    }
                    $params[$pk] = $record->$fk;
                } else {
                    if (is_int($i)) {
                        if (isset($element->_table->foreignKeys[$fk])) // FK defined
                            $pk = $element->_table->foreignKeys[$fk][1];
                        elseif (is_array($parent->_table->primaryKey)) // composite PK
                            $pk = $parent->_table->primaryKey[$i];
                        else
                            $pk = $parent->_table->primaryKey;
                    }
                    $params[$fk] = $record->$pk;
                }
            }
            $count = 0;
            foreach ($params as $name => $value) {
                $query->conditions[] = $prefix . $schema->quoteColumnName($name) . '=:ypl' . $count;
                $query->params[':ypl' . $count] = $value;
                $count++;
            }
        }
    }

    /**
     * Performs the eager loading with the base records ready.
     * @param mixed $baseRecords the available base record(s).
     */
    public function findWithBase($baseRecords)
    {
        if (!is_array($baseRecords))
            $baseRecords = array($baseRecords);
        if (is_string($this->_table->primaryKey)) {
            foreach ($baseRecords as $baseRecord)
                $this->records[$baseRecord->{$this->_table->primaryKey}] = $baseRecord;
        } else {
            foreach ($baseRecords as $baseRecord) {
                $pk = array();
                foreach ($this->_table->primaryKey as $name)
                    $pk[$name] = $baseRecord->$name;
                $this->records[serialize($pk)] = $baseRecord;
            }
        }

        $query = new CJoinQuery($this);
        $this->buildQuery($query);
        if (count($query->joins) > 1)
            $this->runQuery($query);
        foreach ($this->children as $child)
            $child->find();

        foreach ($this->stats as $stat)
            $stat->query();
    }

    /**
     * Count the number of primary records returned by the join statement.
     * @param CDbCriteria $criteria the query criteria
     * @return string number of primary records. Note: type is string to keep max. precision.
     */
    public function count($criteria = null)
    {
        $query = new CJoinQuery($this, $criteria);
        // ensure only one big join statement is used
        $this->_finder->baseLimited = false;
        $this->_finder->joinAll = true;
        $this->buildQuery($query);

        $query->limit = $query->offset = -1;

        if (!empty($criteria->group) || !empty($criteria->having)) {
            $query->orders = array();
            $command = $query->createCommand($this->_builder);
            $sql = $command->getText();
            $sql = "SELECT COUNT(*) FROM ({$sql}) sq";
            $command->setText($sql);
            $command->params = $query->params;
            return $command->queryScalar();
        } else {
            $select = is_array($criteria->select) ? implode(',', $criteria->select) : $criteria->select;
            if ($select !== '*' && !strncasecmp($select, 'count', 5))
                $query->selects = array($select);
            elseif (is_string($this->_table->primaryKey)) {
                $prefix = $this->getColumnPrefix();
                $schema = $this->_builder->getSchema();
                $column = $prefix . $schema->quoteColumnName($this->_table->primaryKey);
                $query->selects = array("COUNT(DISTINCT $column)");
            } else
                $query->selects = array("COUNT(*)");

            $query->orders = $query->groups = $query->havings = array();
            $command = $query->createCommand($this->_builder);
            return $command->queryScalar();
        }
    }

    /**
     * Calls {@link CActiveRecord::afterFind} of all the records.
     */
    public function afterFind()
    {
        foreach ($this->records as $record)
            $record->afterFindInternal();
        foreach ($this->children as $child)
            $child->afterFind();

        $this->children = null;
    }

    /**
     * Builds the join query with all descendant HAS_ONE and BELONGS_TO nodes.
     * @param CJoinQuery $query the query being built up
     */
    public function buildQuery($query)
    {
        foreach ($this->children as $child) {
            if ($child->master !== null)
                $child->_joined = true;
            elseif ($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation
                || $this->_finder->joinAll || $child->relation->together || (!$this->_finder->baseLimited && $child->relation->together === null)
            ) {
                $child->_joined = true;
                $query->join($child);
                $child->buildQuery($query);
            }
        }
    }

    /**
     * Executes the join query and populates the query results.
     * @param CJoinQuery $query the query to be executed.
     */
    public function runQuery($query)
    {
        $command = $query->createCommand($this->_builder);
        foreach ($command->queryAll() as $row)
            $this->populateRecord($query, $row);
    }

    /**
     * Populates the active records with the query data.
     * @param CJoinQuery $query the query executed
     * @param array $row a row of data
     * @return CActiveRecord the populated record
     */
    private function populateRecord($query, $row)
    {
        // determine the primary key value
        if (is_string($this->_pkAlias)) // single key
        {
            if (isset($row[$this->_pkAlias]))
                $pk = $row[$this->_pkAlias];
            else // no matching related objects
                return null;
        } else // is_array, composite key
        {
            $pk = array();
            foreach ($this->_pkAlias as $name => $alias) {
                if (isset($row[$alias]))
                    $pk[$name] = $row[$alias];
                else // no matching related objects
                    return null;
            }
            $pk = serialize($pk);
        }

        // retrieve or populate the record according to the primary key value
        if (isset($this->records[$pk]))
            $record = $this->records[$pk];
        else {
            $attributes = array();
            $aliases = array_flip($this->_columnAliases);
            foreach ($row as $alias => $value) {
                if (isset($aliases[$alias]))
                    $attributes[$aliases[$alias]] = $value;
            }
            $record = $this->model->populateRecord($attributes, false);
            foreach ($this->children as $child) {
                if (!empty($child->relation->select))
                    $record->addRelatedRecord($child->relation->name, null, $child->relation instanceof CHasManyRelation);
            }
            $this->records[$pk] = $record;
        }

        // populate child records recursively
        foreach ($this->children as $child) {
            if (!isset($query->elements[$child->id]) || empty($child->relation->select))
                continue;
            $childRecord = $child->populateRecord($query, $row);
            if ($child->relation instanceof CHasOneRelation || $child->relation instanceof CBelongsToRelation)
                $record->addRelatedRecord($child->relation->name, $childRecord, false);
            else // has_many and many_many
            {
                // need to double check to avoid adding duplicated related objects
                if ($childRecord instanceof CActiveRecord)
                    $fpk = serialize($childRecord->getPrimaryKey());
                else
                    $fpk = 0;
                if (!isset($this->_related[$pk][$child->relation->name][$fpk])) {
                    if ($childRecord instanceof CActiveRecord && $child->relation->index !== null)
                        $index = $childRecord->{$child->relation->index};
                    else
                        $index = true;
                    $record->addRelatedRecord($child->relation->name, $childRecord, $index);
                    $this->_related[$pk][$child->relation->name][$fpk] = true;
                }
            }
        }

        return $record;
    }

    /**
     * @return string the table name and the table alias (if any). This can be used directly in SQL query without escaping.
     */
    public function getTableNameWithAlias()
    {
        if ($this->tableAlias !== null)
            return $this->_table->rawName . ' ' . $this->rawTableAlias;
        else
            return $this->_table->rawName;
    }

    /**
     * Generates the list of columns to be selected.
     * Columns will be properly aliased and primary keys will be added to selection if they are not specified.
     * @param mixed $select columns to be selected. Defaults to '*', indicating all columns.
     * @throws CDbException if active record class is trying to select an invalid column
     * @return string the column selection
     */
    public function getColumnSelect($select = '*')
    {
        $schema = $this->_builder->getSchema();
        $prefix = $this->getColumnPrefix();
        $columns = array();
        if ($select === '*') {
            foreach ($this->_table->getColumnNames() as $name)
                $columns[] = $prefix . $schema->quoteColumnName($name) . ' AS ' . $schema->quoteColumnName($this->_columnAliases[$name]);
        } else {
            if (is_string($select))
                $select = explode(',', $select);
            $selected = array();
            foreach ($select as $name) {
                $name = trim($name);
                $matches = array();
                if (($pos = strrpos($name, '.')) !== false)
                    $key = substr($name, $pos + 1);
                else
                    $key = $name;
                $key = trim($key, '\'"`');

                if ($key === '*') {
                    foreach ($this->_table->columns as $name => $column) {
                        $alias = $this->_columnAliases[$name];
                        if (!isset($selected[$alias])) {
                            $columns[] = $prefix . $column->rawName . ' AS ' . $schema->quoteColumnName($alias);
                            $selected[$alias] = 1;
                        }
                    }
                    continue;
                }

                if (isset($this->_columnAliases[$key])) // simple column names
                {
                    $columns[] = $prefix . $schema->quoteColumnName($key) . ' AS ' . $schema->quoteColumnName($this->_columnAliases[$key]);
                    $selected[$this->_columnAliases[$key]] = 1;
                } elseif (preg_match('/^(.*?)\s+AS\s+(\w+)$/im', $name, $matches)) // if the column is already aliased
                {
                    $alias = $matches[2];
                    if (!isset($this->_columnAliases[$alias]) || $this->_columnAliases[$alias] !== $alias) {
                        $this->_columnAliases[$alias] = $alias;
                        $columns[] = $name;
                        $selected[$alias] = 1;
                    }
                } else
                    throw new CDbException(Yii::t('yii', 'Active record "{class}" is trying to select an invalid column "{column}". Note, the column must exist in the table or be an expression with alias.',
                        array('{class}' => get_class($this->model), '{column}' => $name)));
            }
            // add primary key selection if they are not selected
            if (is_string($this->_pkAlias) && !isset($selected[$this->_pkAlias]))
                $columns[] = $prefix . $schema->quoteColumnName($this->_table->primaryKey) . ' AS ' . $schema->quoteColumnName($this->_pkAlias);
            elseif (is_array($this->_pkAlias)) {
                foreach ($this->_table->primaryKey as $name)
                    if (!isset($selected[$name]))
                        $columns[] = $prefix . $schema->quoteColumnName($name) . ' AS ' . $schema->quoteColumnName($this->_pkAlias[$name]);
            }
        }

        return implode(', ', $columns);
    }

    /**
     * @return string the primary key selection
     */
    public function getPrimaryKeySelect()
    {
        $schema = $this->_builder->getSchema();
        $prefix = $this->getColumnPrefix();
        $columns = array();
        if (is_string($this->_pkAlias))
            $columns[] = $prefix . $schema->quoteColumnName($this->_table->primaryKey) . ' AS ' . $schema->quoteColumnName($this->_pkAlias);
        elseif (is_array($this->_pkAlias)) {
            foreach ($this->_pkAlias as $name => $alias)
                $columns[] = $prefix . $schema->quoteColumnName($name) . ' AS ' . $schema->quoteColumnName($alias);
        }
        return implode(', ', $columns);
    }

    /**
     * @return string the condition that specifies only the rows with the selected primary key values.
     */
    public function getPrimaryKeyRange()
    {
        if (empty($this->records))
            return '';
        $values = array_keys($this->records);
        if (is_array($this->_table->primaryKey)) {
            foreach ($values as &$value)
                $value = unserialize($value);
        }
        return $this->_builder->createInCondition($this->_table, $this->_table->primaryKey, $values, $this->getColumnPrefix());
    }

    /**
     * @return string the column prefix for column reference disambiguation
     */
    public function getColumnPrefix()
    {
        if ($this->tableAlias !== null)
            return $this->rawTableAlias . '.';
        else
            return $this->_table->rawName . '.';
    }

    /**
     * @throws CDbException if relation in active record class is not specified correctly
     * @return string the join statement (this node joins with its parent)
     */
    public function getJoinCondition()
    {
        $parent = $this->_parent;
        if ($this->relation instanceof CManyManyRelation) {
            $schema = $this->_builder->getSchema();
            $joinTableName = $this->relation->getJunctionTableName();
            if (($joinTable = $schema->getTable($joinTableName)) === null)
                throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.',
                    array('{class}' => get_class($parent->model), '{relation}' => $this->relation->name, '{joinTable}' => $joinTableName)));
            $fks = $this->relation->getJunctionForeignKeys();

            return $this->joinManyMany($joinTable, $fks, $parent);
        } else {
            $fks = is_array($this->relation->foreignKey) ? $this->relation->foreignKey : preg_split('/\s*,\s*/', $this->relation->foreignKey, -1, PREG_SPLIT_NO_EMPTY);
            if ($this->slave !== null) {
                if ($this->relation instanceof CBelongsToRelation) {
                    $fks = array_flip($fks);
                    $pke = $this->slave;
                    $fke = $this;
                } else {
                    $pke = $this;
                    $fke = $this->slave;
                }
            } elseif ($this->relation instanceof CBelongsToRelation) {
                $pke = $this;
                $fke = $parent;
            } else {
                $pke = $parent;
                $fke = $this;
            }
            return $this->joinOneMany($fke, $fks, $pke, $parent);
        }
    }

    /**
     * Generates the join statement for one-many relationship.
     * This works for HAS_ONE, HAS_MANY and BELONGS_TO.
     * @param CJoinElement $fke the join element containing foreign keys
     * @param array $fks the foreign keys
     * @param CJoinElement $pke the join element contains primary keys
     * @param CJoinElement $parent the parent join element
     * @return string the join statement
     * @throws CDbException if a foreign key is invalid
     */
    private function joinOneMany($fke, $fks, $pke, $parent)
    {
        $schema = $this->_builder->getSchema();
        $joins = array();
        if (is_string($fks))
            $fks = preg_split('/\s*,\s*/', $fks, -1, PREG_SPLIT_NO_EMPTY);
        foreach ($fks as $i => $fk) {
            if (!is_int($i)) {
                $pk = $fk;
                $fk = $i;
            }

            if (!isset($fke->_table->columns[$fk]))
                throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".',
                    array('{class}' => get_class($parent->model), '{relation}' => $this->relation->name, '{key}' => $fk, '{table}' => $fke->_table->name)));

            if (is_int($i)) {
                if (isset($fke->_table->foreignKeys[$fk]) && $schema->compareTableNames($pke->_table->rawName, $fke->_table->foreignKeys[$fk][0]))
                    $pk = $fke->_table->foreignKeys[$fk][1];
                else // FK constraints undefined
                {
                    if (is_array($pke->_table->primaryKey)) // composite PK
                        $pk = $pke->_table->primaryKey[$i];
                    else
                        $pk = $pke->_table->primaryKey;
                }
            }

            $joins[] = $fke->getColumnPrefix() . $schema->quoteColumnName($fk) . '=' . $pke->getColumnPrefix() . $schema->quoteColumnName($pk);
        }
        if (!empty($this->relation->on))
            $joins[] = $this->relation->on;
        return $this->relation->joinType . ' ' . $this->getTableNameWithAlias() . ' ON (' . implode(') AND (', $joins) . ')';
    }

    /**
     * Generates the join statement for many-many relationship.
     * @param CDbTableSchema $joinTable the join table
     * @param array $fks the foreign keys
     * @param CJoinElement $parent the parent join element
     * @return string the join statement
     * @throws CDbException if a foreign key is invalid
     */
    private function joinManyMany($joinTable, $fks, $parent)
    {
        $schema = $this->_builder->getSchema();
        $joinAlias = $schema->quoteTableName($this->relation->name . '_' . $this->tableAlias);
        $parentCondition = array();
        $childCondition = array();

        $fkDefined = true;
        foreach ($fks as $i => $fk) {
            if (!isset($joinTable->columns[$fk]))
                throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".',
                    array('{class}' => get_class($parent->model), '{relation}' => $this->relation->name, '{key}' => $fk, '{table}' => $joinTable->name)));

            if (isset($joinTable->foreignKeys[$fk])) {
                list($tableName, $pk) = $joinTable->foreignKeys[$fk];
                if (!isset($parentCondition[$pk]) && $schema->compareTableNames($parent->_table->rawName, $tableName))
                    $parentCondition[$pk] = $parent->getColumnPrefix() . $schema->quoteColumnName($pk) . '=' . $joinAlias . '.' . $schema->quoteColumnName($fk);
                elseif (!isset($childCondition[$pk]) && $schema->compareTableNames($this->_table->rawName, $tableName))
                    $childCondition[$pk] = $this->getColumnPrefix() . $schema->quoteColumnName($pk) . '=' . $joinAlias . '.' . $schema->quoteColumnName($fk);
                else {
                    $fkDefined = false;
                    break;
                }
            } else {
                $fkDefined = false;
                break;
            }
        }

        if (!$fkDefined) {
            $parentCondition = array();
            $childCondition = array();
            foreach ($fks as $i => $fk) {
                if ($i < count($parent->_table->primaryKey)) {
                    $pk = is_array($parent->_table->primaryKey) ? $parent->_table->primaryKey[$i] : $parent->_table->primaryKey;
                    $parentCondition[$pk] = $parent->getColumnPrefix() . $schema->quoteColumnName($pk) . '=' . $joinAlias . '.' . $schema->quoteColumnName($fk);
                } else {
                    $j = $i - count($parent->_table->primaryKey);
                    $pk = is_array($this->_table->primaryKey) ? $this->_table->primaryKey[$j] : $this->_table->primaryKey;
                    $childCondition[$pk] = $this->getColumnPrefix() . $schema->quoteColumnName($pk) . '=' . $joinAlias . '.' . $schema->quoteColumnName($fk);
                }
            }
        }

        if ($parentCondition !== array() && $childCondition !== array()) {
            $join = $this->relation->joinType . ' ' . $joinTable->rawName . ' ' . $joinAlias;
            $join .= ' ON (' . implode(') AND (', $parentCondition) . ')';
            $join .= ' ' . $this->relation->joinType . ' ' . $this->getTableNameWithAlias();
            $join .= ' ON (' . implode(') AND (', $childCondition) . ')';
            if (!empty($this->relation->on))
                $join .= ' AND (' . $this->relation->on . ')';
            return $join;
        } else
            throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
                array('{class}' => get_class($parent->model), '{relation}' => $this->relation->name)));
    }
}


/**
 * CJoinQuery represents a JOIN SQL statement.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.db.ar
 * @since 1.0
 */
class CJoinQuery
{
    /**
     * @var array list of column selections
     */
    public $selects = array();
    /**
     * @var boolean whether to select distinct result set
     */
    public $distinct = false;
    /**
     * @var array list of join statement
     */
    public $joins = array();
    /**
     * @var array list of WHERE clauses
     */
    public $conditions = array();
    /**
     * @var array list of ORDER BY clauses
     */
    public $orders = array();
    /**
     * @var array list of GROUP BY clauses
     */
    public $groups = array();
    /**
     * @var array list of HAVING clauses
     */
    public $havings = array();
    /**
     * @var integer row limit
     */
    public $limit = -1;
    /**
     * @var integer row offset
     */
    public $offset = -1;
    /**
     * @var array list of query parameters
     */
    public $params = array();
    /**
     * @var array list of join element IDs (id=>true)
     */
    public $elements = array();

    /**
     * Constructor.
     * @param CJoinElement $joinElement The root join tree.
     * @param CDbCriteria $criteria the query criteria
     */
    public function __construct($joinElement, $criteria = null)
    {
        if ($criteria !== null) {
            $this->selects[] = $joinElement->getColumnSelect($criteria->select);
            $this->joins[] = $joinElement->getTableNameWithAlias();
            $this->joins[] = $criteria->join;
            $this->conditions[] = $criteria->condition;
            $this->orders[] = $criteria->order;
            $this->groups[] = $criteria->group;
            $this->havings[] = $criteria->having;
            $this->limit = $criteria->limit;
            $this->offset = $criteria->offset;
            $this->params = $criteria->params;
            if (!$this->distinct && $criteria->distinct)
                $this->distinct = true;
        } else {
            $this->selects[] = $joinElement->getPrimaryKeySelect();
            $this->joins[] = $joinElement->getTableNameWithAlias();
            $this->conditions[] = $joinElement->getPrimaryKeyRange();
        }
        $this->elements[$joinElement->id] = true;
    }

    /**
     * Joins with another join element
     * @param CJoinElement $element the element to be joined
     */
    public function join($element)
    {
        if ($element->slave !== null)
            $this->join($element->slave);
        if (!empty($element->relation->select))
            $this->selects[] = $element->getColumnSelect($element->relation->select);
        $this->conditions[] = $element->relation->condition;
        $this->orders[] = $element->relation->order;
        $this->joins[] = $element->getJoinCondition();
        $this->joins[] = $element->relation->join;
        $this->groups[] = $element->relation->group;
        $this->havings[] = $element->relation->having;

        if (is_array($element->relation->params)) {
            if (is_array($this->params))
                $this->params = array_merge($this->params, $element->relation->params);
            else
                $this->params = $element->relation->params;
        }
        $this->elements[$element->id] = true;
    }

    /**
     * Creates the SQL statement.
     * @param CDbCommandBuilder $builder the command builder
     * @return CDbCommand DB command instance representing the SQL statement
     */
    public function createCommand($builder)
    {
        $sql = ($this->distinct ? 'SELECT DISTINCT ' : 'SELECT ') . implode(', ', $this->selects);
        $sql .= ' FROM ' . implode(' ', $this->joins);

        $conditions = array();
        foreach ($this->conditions as $condition)
            if ($condition !== '')
                $conditions[] = $condition;
        if ($conditions !== array())
            $sql .= ' WHERE (' . implode(') AND (', $conditions) . ')';

        $groups = array();
        foreach ($this->groups as $group)
            if ($group !== '')
                $groups[] = $group;
        if ($groups !== array())
            $sql .= ' GROUP BY ' . implode(', ', $groups);

        $havings = array();
        foreach ($this->havings as $having)
            if ($having !== '')
                $havings[] = $having;
        if ($havings !== array())
            $sql .= ' HAVING (' . implode(') AND (', $havings) . ')';

        $orders = array();
        foreach ($this->orders as $order)
            if ($order !== '')
                $orders[] = $order;
        if ($orders !== array())
            $sql .= ' ORDER BY ' . implode(', ', $orders);

        $sql = $builder->applyLimit($sql, $this->limit, $this->offset);
        $command = $builder->getDbConnection()->createCommand($sql);
        $builder->bindValues($command, $this->params);
        return $command;
    }
}


/**
 * CStatElement represents STAT join element for {@link CActiveFinder}.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @package system.db.ar
 */
class CStatElement
{
    /**
     * @var CActiveRelation the relation represented by this tree node
     */
    public $relation;

    private $_finder;
    private $_parent;

    /**
     * Constructor.
     * @param CActiveFinder $finder the finder
     * @param CStatRelation $relation the STAT relation
     * @param CJoinElement $parent the join element owning this STAT element
     */
    public function __construct($finder, $relation, $parent)
    {
        $this->_finder = $finder;
        $this->_parent = $parent;
        $this->relation = $relation;
        $parent->stats[] = $this;
    }

    /**
     * Performs the STAT query.
     */
    public function query()
    {
        if (preg_match('/^\s*(.*?)\((.*)\)\s*$/', $this->relation->foreignKey, $matches))
            $this->queryManyMany($matches[1], $matches[2]);
        else
            $this->queryOneMany();
    }

    private function queryOneMany()
    {
        $relation = $this->relation;
        $model = $this->_finder->getModel($relation->className);
        $builder = $model->getCommandBuilder();
        $schema = $builder->getSchema();
        $table = $model->getTableSchema();
        $parent = $this->_parent;
        $pkTable = $parent->model->getTableSchema();

        $fks = preg_split('/\s*,\s*/', $relation->foreignKey, -1, PREG_SPLIT_NO_EMPTY);
        if (count($fks) !== count($pkTable->primaryKey))
            throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key. The columns in the key must match the primary keys of the table "{table}".',
                array('{class}' => get_class($parent->model), '{relation}' => $relation->name, '{table}' => $pkTable->name)));

        // set up mapping between fk and pk columns
        $map = array(); // pk=>fk
        foreach ($fks as $i => $fk) {
            if (!isset($table->columns[$fk]))
                throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".',
                    array('{class}' => get_class($parent->model), '{relation}' => $relation->name, '{key}' => $fk, '{table}' => $table->name)));

            if (isset($table->foreignKeys[$fk])) {
                list($tableName, $pk) = $table->foreignKeys[$fk];
                if ($schema->compareTableNames($pkTable->rawName, $tableName))
                    $map[$pk] = $fk;
                else
                    throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with a foreign key "{key}" that does not point to the parent table "{table}".',
                        array('{class}' => get_class($parent->model), '{relation}' => $relation->name, '{key}' => $fk, '{table}' => $pkTable->name)));
            } else // FK constraints undefined
            {
                if (is_array($pkTable->primaryKey)) // composite PK
                    $map[$pkTable->primaryKey[$i]] = $fk;
                else
                    $map[$pkTable->primaryKey] = $fk;
            }
        }

        $records = $this->_parent->records;

        $join = empty($relation->join) ? '' : ' ' . $relation->join;
        $where = empty($relation->condition) ? ' WHERE ' : ' WHERE (' . $relation->condition . ') AND ';
        $group = empty($relation->group) ? '' : ', ' . $relation->group;
        $having = empty($relation->having) ? '' : ' HAVING (' . $relation->having . ')';
        $order = empty($relation->order) ? '' : ' ORDER BY ' . $relation->order;

        $c = $schema->quoteColumnName('c');
        $s = $schema->quoteColumnName('s');

        $tableAlias = $model->getTableAlias(true);

        // generate and perform query
        if (count($fks) === 1) // single column FK
        {
            $col = $tableAlias . '.' . $table->columns[$fks[0]]->rawName;
            $sql = "SELECT $col AS $c, {$relation->select} AS $s FROM {$table->rawName} " . $tableAlias . $join
                . $where . '(' . $builder->createInCondition($table, $fks[0], array_keys($records), $tableAlias . '.') . ')'
                . " GROUP BY $col" . $group
                . $having . $order;
            $command = $builder->getDbConnection()->createCommand($sql);
            if (is_array($relation->params))
                $builder->bindValues($command, $relation->params);
            $stats = array();
            foreach ($command->queryAll() as $row)
                $stats[$row['c']] = $row['s'];
        } else // composite FK
        {
            $keys = array_keys($records);
            foreach ($keys as &$key) {
                $key2 = unserialize($key);
                $key = array();
                foreach ($pkTable->primaryKey as $pk)
                    $key[$map[$pk]] = $key2[$pk];
            }
            $cols = array();
            foreach ($pkTable->primaryKey as $n => $pk) {
                $name = $tableAlias . '.' . $table->columns[$map[$pk]]->rawName;
                $cols[$name] = $name . ' AS ' . $schema->quoteColumnName('c' . $n);
            }
            $sql = 'SELECT ' . implode(', ', $cols) . ", {$relation->select} AS $s FROM {$table->rawName} " . $tableAlias . $join
                . $where . '(' . $builder->createInCondition($table, $fks, $keys, $tableAlias . '.') . ')'
                . ' GROUP BY ' . implode(', ', array_keys($cols)) . $group
                . $having . $order;
            $command = $builder->getDbConnection()->createCommand($sql);
            if (is_array($relation->params))
                $builder->bindValues($command, $relation->params);
            $stats = array();
            foreach ($command->queryAll() as $row) {
                $key = array();
                foreach ($pkTable->primaryKey as $n => $pk)
                    $key[$pk] = $row['c' . $n];
                $stats[serialize($key)] = $row['s'];
            }
        }

        // populate the results into existing records
        foreach ($records as $pk => $record)
            $record->addRelatedRecord($relation->name, isset($stats[$pk]) ? $stats[$pk] : $relation->defaultValue, false);
    }

    /*
	 * @param string $joinTableName jointablename
	 * @param string $keys keys
	 */
    private function queryManyMany($joinTableName, $keys)
    {
        $relation = $this->relation;
        $model = $this->_finder->getModel($relation->className);
        $table = $model->getTableSchema();
        $builder = $model->getCommandBuilder();
        $schema = $builder->getSchema();
        $pkTable = $this->_parent->model->getTableSchema();

        $tableAlias = $model->getTableAlias(true);

        if (($joinTable = $builder->getSchema()->getTable($joinTableName)) === null)
            throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is not specified correctly: the join table "{joinTable}" given in the foreign key cannot be found in the database.',
                array('{class}' => get_class($this->_parent->model), '{relation}' => $relation->name, '{joinTable}' => $joinTableName)));

        $fks = preg_split('/\s*,\s*/', $keys, -1, PREG_SPLIT_NO_EMPTY);
        if (count($fks) !== count($table->primaryKey) + count($pkTable->primaryKey))
            throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
                array('{class}' => get_class($this->_parent->model), '{relation}' => $relation->name)));

        $joinCondition = array();
        $map = array();

        $fkDefined = true;
        foreach ($fks as $i => $fk) {
            if (!isset($joinTable->columns[$fk]))
                throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an invalid foreign key "{key}". There is no such column in the table "{table}".',
                    array('{class}' => get_class($this->_parent->model), '{relation}' => $relation->name, '{key}' => $fk, '{table}' => $joinTable->name)));

            if (isset($joinTable->foreignKeys[$fk])) {
                list($tableName, $pk) = $joinTable->foreignKeys[$fk];
                if (!isset($joinCondition[$pk]) && $schema->compareTableNames($table->rawName, $tableName))
                    $joinCondition[$pk] = $tableAlias . '.' . $schema->quoteColumnName($pk) . '=' . $joinTable->rawName . '.' . $schema->quoteColumnName($fk);
                elseif (!isset($map[$pk]) && $schema->compareTableNames($pkTable->rawName, $tableName))
                    $map[$pk] = $fk;
                else {
                    $fkDefined = false;
                    break;
                }
            } else {
                $fkDefined = false;
                break;
            }
        }

        if (!$fkDefined) {
            $joinCondition = array();
            $map = array();
            foreach ($fks as $i => $fk) {
                if ($i < count($pkTable->primaryKey)) {
                    $pk = is_array($pkTable->primaryKey) ? $pkTable->primaryKey[$i] : $pkTable->primaryKey;
                    $map[$pk] = $fk;
                } else {
                    $j = $i - count($pkTable->primaryKey);
                    $pk = is_array($table->primaryKey) ? $table->primaryKey[$j] : $table->primaryKey;
                    $joinCondition[$pk] = $tableAlias . '.' . $schema->quoteColumnName($pk) . '=' . $joinTable->rawName . '.' . $schema->quoteColumnName($fk);
                }
            }
        }

        if ($joinCondition === array() || $map === array())
            throw new CDbException(Yii::t('yii', 'The relation "{relation}" in active record class "{class}" is specified with an incomplete foreign key. The foreign key must consist of columns referencing both joining tables.',
                array('{class}' => get_class($this->_parent->model), '{relation}' => $relation->name)));

        $records = $this->_parent->records;

        $cols = array();
        foreach (is_string($pkTable->primaryKey) ? array($pkTable->primaryKey) : $pkTable->primaryKey as $n => $pk) {
            $name = $joinTable->rawName . '.' . $schema->quoteColumnName($map[$pk]);
            $cols[$name] = $name . ' AS ' . $schema->quoteColumnName('c' . $n);
        }

        $keys = array_keys($records);
        if (is_array($pkTable->primaryKey)) {
            foreach ($keys as &$key) {
                $key2 = unserialize($key);
                $key = array();
                foreach ($pkTable->primaryKey as $pk)
                    $key[$map[$pk]] = $key2[$pk];
            }
        }

        $join = empty($relation->join) ? '' : ' ' . $relation->join;
        $where = empty($relation->condition) ? '' : ' WHERE (' . $relation->condition . ')';
        $group = empty($relation->group) ? '' : ', ' . $relation->group;
        $having = empty($relation->having) ? '' : ' AND (' . $relation->having . ')';
        $order = empty($relation->order) ? '' : ' ORDER BY ' . $relation->order;

        $sql = 'SELECT ' . $this->relation->select . ' AS ' . $schema->quoteColumnName('s') . ', ' . implode(', ', $cols)
            . ' FROM ' . $table->rawName . ' ' . $tableAlias . ' INNER JOIN ' . $joinTable->rawName
            . ' ON (' . implode(') AND (', $joinCondition) . ')' . $join
            . $where
            . ' GROUP BY ' . implode(', ', array_keys($cols)) . $group
            . ' HAVING (' . $builder->createInCondition($joinTable, $map, $keys) . ')'
            . $having . $order;

        $command = $builder->getDbConnection()->createCommand($sql);
        if (is_array($relation->params))
            $builder->bindValues($command, $relation->params);

        $stats = array();
        foreach ($command->queryAll() as $row) {
            if (is_array($pkTable->primaryKey)) {
                $key = array();
                foreach ($pkTable->primaryKey as $n => $k)
                    $key[$k] = $row['c' . $n];
                $stats[serialize($key)] = $row['s'];
            } else
                $stats[$row['c0']] = $row['s'];
        }

        foreach ($records as $pk => $record)
            $record->addRelatedRecord($relation->name, isset($stats[$pk]) ? $stats[$pk] : $this->relation->defaultValue, false);
    }
}
